﻿using System;
using System.Globalization;
using System.Text;
using System.Xml;

using octalforty.Syndication.Core;
using octalforty.Syndication.Core.Resources;

namespace octalforty.Syndication.Core.Rss
{
    /// <summary>
    /// Represents a <see cref="ISyndicator"/> with support for RSS 2.0.8.
    /// </summary>
    /// <remarks>
    /// The specification is available at http://www.rssboard.org/rss-specification. It is also
    /// possible that the mentioned URL will refer to a newer version of the specification, so
    /// the 2.0.8 version might be available at http://www.rssboard.org/rss-2-0-1-rv-8.<para />
    /// The RSS 2.0 XSD courtesy of <cite>wkriebel</cite> (http://weblogs.asp.net/wkriebel/archive/2004/03/07/85642.aspx).
    /// </remarks>
    public class Rss208Syndicator : ISyndicator
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="Rss208Syndicator"/> class.
        /// </summary>
        public Rss208Syndicator()
        {
        }

        #region ISyndicator Members
        /// <summary>
        /// Syndicates <paramref name="syndicationFeed"/> and produces an <see cref="XmlDocument"/>
        /// of the appropriate format.
        /// </summary>
        /// <param name="syndicationFeed">The <see cref="ISyndicationFeed"/> to be syndicated.</param>
        /// <returns></returns>
        /// <exception cref="SyndicationException">
        /// When the RSS specification has been violated (that is, when <paramref name="syndicationFeed"/>
        /// is missing required properties).
        /// </exception>
        public virtual XmlDocument Syndicate(ISyndicationFeed syndicationFeed)
        {
            if(syndicationFeed == null)
                throw new ArgumentNullException("syndicationFeed");

            //
            // First, check whether Title, Link and Description are neither null
            // nor empty. This is the requirement of RSS spec.
            if(String.IsNullOrEmpty(syndicationFeed.Title))
                throw new SyndicationException(Strings.Rss_Rss208Syndicator_SyndicationFeedTitleMissing);

            if(String.IsNullOrEmpty(syndicationFeed.Description))
                throw new SyndicationException(
                    Strings.Rss_Rss208Syndicator_SyndicationFeedDescriptionMissing);

            if(String.IsNullOrEmpty(syndicationFeed.Link))
                throw new SyndicationException(
                    Strings.Rss_Rss208Syndicator_SyndicationFeedLinkMissing);

            try
            {
                //
                // Now we're ready to syndicate the feed.
                XmlDocument feedDocument = new XmlDocument();
                SyndicateFeed(syndicationFeed, feedDocument);

                return feedDocument;
            } // try

            catch(Exception e)
            {
                throw new SyndicationException(
                    Strings.Rss_Rss208Syndicator_SyndicationFailed, e);
            } // catch
        }
        #endregion

        /// <summary>
        /// Creates an instance of the class which implements <see cref="ISyndicationFeed"/> with
        /// all the properties required by the RSS specification.
        /// </summary>
        /// <param name="title"></param>
        /// <param name="link"></param>
        /// <param name="description"></param> 
        /// <returns></returns>
        public static ISyndicationFeed CreateSyndicationFeed(String title, String link, String description)
        {
            SyndicationFeed syndicationFeed = new SyndicationFeed();

            syndicationFeed.Title = title;
            syndicationFeed.Link = link;
            syndicationFeed.Description = description;

            return syndicationFeed;
        }

        /// <summary>
        /// Creates an instance of the class which implements <see cref="ISyndicationFeedItem"/> with
        /// all properties required by the RSS specification.
        /// </summary>
        /// <param name="title"></param>
        /// <param name="link"></param>
        /// <param name="description"></param>
        /// <returns></returns>
        public static ISyndicationFeedItem CreateSyndicationFeedItem(String title, String link, 
            String description)
        {
            ISyndicationFeedItem syndicationFeedItem = new SyndicationFeedItem();

            syndicationFeedItem.Title = title;
            syndicationFeedItem.Link = link;
            syndicationFeedItem.Description = description;

            return syndicationFeedItem;
        }

        private static void SyndicateFeed(ISyndicationFeed syndicationFeed, XmlDocument feedDocument)
        {
            if(syndicationFeed == null)
                throw new ArgumentNullException("syndicationFeed");

            if(feedDocument == null)
                throw new ArgumentNullException("feedDocument");

            //
            // Creating document element, and root channel element.
            XmlElement rssElement = feedDocument.CreateElement("rss");
                        
            AppendAttribute(rssElement, "version", "2.0");
            feedDocument.AppendChild(rssElement);

            XmlElement channelElement = feedDocument.CreateElement("channel");
            rssElement.AppendChild(channelElement);

            //
            // Exporing required channel elements.
            AppendElement(channelElement, "title", syndicationFeed.Title);
            AppendElement(channelElement, "link", syndicationFeed.Description);
            AppendElement(channelElement, "description", syndicationFeed.Description);

            if(!String.IsNullOrEmpty(syndicationFeed.Copyright))
                AppendElement(channelElement, "copyright", syndicationFeed.Copyright);

            if(syndicationFeed.PublishedOn.HasValue)
                AppendElement(channelElement, "pubDate", 
                    syndicationFeed.PublishedOn.Value.ToString("r", DateTimeFormatInfo.InvariantInfo));

            if(syndicationFeed.UpdatedOn.HasValue)
                AppendElement(channelElement, "lastBuildDate", 
                    syndicationFeed.UpdatedOn.Value.ToString("r", DateTimeFormatInfo.InvariantInfo));

            //
            // Author and Contributor are ignored for RSS, so that only WebMaster
            // and ManagingEditor are syndicated.
            if(syndicationFeed.ManagingEditor != null)
                AppendElement(channelElement, "managingEditor",
                    SyndicatePerson(syndicationFeed.ManagingEditor));

            if(syndicationFeed.WebMaster != null)
                AppendElement(channelElement, "webMaster",
                    SyndicatePerson(syndicationFeed.WebMaster));

            //
            // Done with channel element. Move on to items.
            foreach(ISyndicationFeedItem item in syndicationFeed.Items)
            {
                channelElement.AppendChild(SyndicateItem(item, feedDocument));
            } // foreach
        }

        private static XmlNode SyndicateItem(ISyndicationFeedItem feedItem, 
            XmlDocument feedDocument)
        {
            if(feedItem == null)
                throw new ArgumentNullException("feedItem");

            if(feedDocument == null)
                throw new ArgumentNullException("feedDocument");

            //
            // First off, check required elements of feedItem according
            // to RSS spec.
            if(String.IsNullOrEmpty(feedItem.Title))
                throw new SyndicationException(
                    Strings.Rss_Rss208Syndicator_SyndicationFeedItemTitleMissing);

            if(String.IsNullOrEmpty(feedItem.Link))
                throw new SyndicationException(
                    Strings.Rss_Rss208Syndicator_SyndicationFeedItemLinkMissing);

            if(String.IsNullOrEmpty(feedItem.Description))
                throw new SyndicationException(
                    Strings.Rss_Rss208Syndicator_SyndicationFeedItemDescriptionMissing);

            //
            // If we got here, everything's fine at least with required elements. So
            // go on and syndicate them.
            XmlElement feedItemElement = feedDocument.CreateElement("item");

            AppendElement(feedItemElement, "title", feedItem.Title);
            AppendElement(feedItemElement, "link", feedItem.Link);
            AppendElement(feedItemElement, "description", feedItem.Description);

            if(feedItem.Author != null)
                AppendElement(feedItemElement, "author", SyndicatePerson(feedItem.Author));

            if(!String.IsNullOrEmpty(feedItem.UniqueIdentifier))
                AppendElement(feedItemElement, "guid", feedItem.UniqueIdentifier);

            if(feedItem.PublishedOn.HasValue)
                AppendElement(feedItemElement, "pubDate",
                    feedItem.PublishedOn.Value.ToString("r", DateTimeFormatInfo.InvariantInfo));

            return feedItemElement;
        }

        private static string SyndicatePerson(IPerson person)
        {
            if(person == null)
                throw new ArgumentNullException("person");

            StringBuilder personBuilder = new StringBuilder(person.FullName);

            if(!String.IsNullOrEmpty(person.EmailAddress))
                personBuilder.AppendFormat("<{0}>", person.EmailAddress);

            return personBuilder.ToString();
        }

        private static void AppendAttribute(XmlElement xmlElement, string name, string value)
        {
            XmlAttribute xmlAttribute = xmlElement.OwnerDocument.CreateAttribute(name);
            xmlAttribute.Value = value;

            xmlElement.Attributes.Append(xmlAttribute);
        }

        private static void AppendElement(XmlElement parentElement, string name, string innerText)
        {
            XmlElement xmlElement = parentElement.OwnerDocument.CreateElement(name);
            xmlElement.InnerText = innerText;

            parentElement.AppendChild(xmlElement);
        }
    }
}
